<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI语音对话系统</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            background-color: #f5f5f5;
            margin: 0;
            padding: 0;
            min-height: 100vh;
        }

        .container {
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }

        h1 {
            text-align: center;
            margin-bottom: 30px;
            color: #333;
        }

        .card {
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            margin-bottom: 20px;
        }

        .card-header {
            font-weight: bold;
            background-color: #f8f9fa;
            border-bottom: 1px solid #dee2e6;
        }

        .card-body {
            padding: 25px;
        }

        .form-control {
            margin-bottom: 15px;
        }

        .btn {
            padding: 8px 16px;
            border-radius: 5px;
            cursor: pointer;
            font-weight: 500;
        }

        .btn-primary {
            background-color: #007bff;
            border: none;
        }

        .btn-primary:hover {
            background-color: #0056b3;
        }

        .status {
            padding: 10px;
            border-radius: 5px;
            margin-top: 10px;
            font-size: 0.9rem;
        }

        .status-success {
            background-color: #d4edda;
            color: #155724;
        }

        .status-error {
            background-color: #f8d7da;
            color: #721c24;
        }

        .status-info {
            background-color: #d1ecf1;
            color: #0c5460;
        }

        .response-container {
            min-height: 100px;
            border: 1px solid #dee2e6;
            border-radius: 5px;
            padding: 15px;
            background-color: #fff;
            margin-top: 15px;
            margin-bottom: 15px;
        }

        .audio-player {
            margin-top: 15px;
            width: 100%;
        }

        .button-container {
            display: flex;
            gap: 10px;
            margin-bottom: 15px;
        }

        .live-indicator {
            display: inline-block;
            width: 10px;
            height: 10px;
            border-radius: 50%;
            background-color: #28a745;
            margin-right: 5px;
            animation: blink 1s infinite;
        }

        @keyframes blink {
            0%, 50% {
                opacity: 1;
            }
            51%, 100% {
                opacity: 0.3;
            }
        }

        .file-info {
            font-size: 0.85rem;
            color: #6c757d;
            margin-top: 5px;
        }
    </style>
</head>
<body>
<div class="container mt-5">
    <h1>AI语音对话系统</h1>

    <!-- 用户录音语音对话模块 -->
    <div class="card">
        <div class="card-header">用户录音语音对话</div>
        <div class="card-body">
            <!-- 录音控制按钮组 -->
            <div class="button-container">
                <button id="startRecordBtn" class="btn btn-success">开始录音</button>
                <button id="stopRecordBtn" class="btn btn-danger" disabled>停止录音</button>
                <button id="startVoiceChatBtn" class="btn btn-primary" disabled>开始对话(HTTP)</button>
                <button id="startStreamingVoiceChatBtn" class="btn btn-primary" disabled>开始流式对话(WebSocket)
                </button>
            </div>

            <div id="chatStatus" class="status status-info">
                <strong>i 就绪:</strong> 点击开始录音按钮开始对话流程
            </div>

            <div id="fileInfo" class="file-info"></div>

            <div class="mt-4">
                <h5>AI回复内容:</h5>
                <div id="aiResponse" class="response-container">
                    AI回复将显示在这里...
                </div>
            </div>

            <div class="mt-4">
                <h5>AI语音回复:</h5>
                <audio id="aiAudioPlayer" class="audio-player" controls>
                    <source src="" type="audio/mpeg">
                    您的浏览器不支持音频播放
                </audio>
            </div>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
    // 全局变量
    let mediaStream = null;
    let audioContext = null;
    let microphone = null;
    let scriptProcessor = null;
    let pcmData = [];
    let savedFilePath = '';
    let isProcessing = false;
    let ws = null; // WebSocket连接
    // 新增变量：用于实时音频播放
    let mediaSource = null;
    let sourceBuffer = null;
    let audioQueue = [];
    let isAudioPlaying = false;
    let audioUrl = null;

    // DOM元素
    const startRecordBtn = document.getElementById('startRecordBtn');
    const stopRecordBtn = document.getElementById('stopRecordBtn');
    const startVoiceChatBtn = document.getElementById('startVoiceChatBtn');
    const startStreamingVoiceChatBtn = document.getElementById('startStreamingVoiceChatBtn');
    const chatStatus = document.getElementById('chatStatus');
    const aiResponse = document.getElementById('aiResponse');
    const aiAudioPlayer = document.getElementById('aiAudioPlayer');
    const fileInfo = document.getElementById('fileInfo');

    // 开始录音
    startRecordBtn.addEventListener('click', async () => {
        try {
            // 检查浏览器支持
            if (!('getUserMedia' in navigator.mediaDevices)) {
                showStatus(chatStatus, '错误', '浏览器不支持麦克风采集功能', 'error');
                return;
            }

            // 获取用户媒体设备（麦克风）
            mediaStream = await navigator.mediaDevices.getUserMedia({
                audio: {
                    echoCancellation: true,
                    noiseSuppression: true,
                    sampleRate: 16000
                }
            });

            // 创建音频上下文
            audioContext = new (window.AudioContext || window.webkitAudioContext)({
                sampleRate: 16000  // 设置采样率为16kHz
            });

            // 创建麦克风输入节点
            microphone = audioContext.createMediaStreamSource(mediaStream);

            // 创建音频处理器节点
            scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);

            // 清空之前的PCM数据
            pcmData = [];

            // 音频处理回调函数
            scriptProcessor.onaudioprocess = function (event) {
                // 获取输入音频数据（单声道）
                const inputData = event.inputBuffer.getChannelData(0);

                // 将Float32数据转换为16位PCM整数
                const int16Data = new Int16Array(inputData.length);
                for (let i = 0; i < inputData.length; i++) {
                    // 音频数据归一化并转换为16位整数
                    const s = Math.max(-1, Math.min(1, inputData[i]));
                    int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
                }

                // 存储PCM数据
                pcmData.push(...int16Data);
            };

            // 连接音频节点
            microphone.connect(scriptProcessor);
            scriptProcessor.connect(audioContext.destination);

            // 更新UI状态
            startRecordBtn.disabled = true;
            stopRecordBtn.disabled = false;
            startVoiceChatBtn.disabled = true;
            startStreamingVoiceChatBtn.disabled = true;
            showStatus(chatStatus, '录音中', '<span class="live-indicator"></span>正在录音，请说话...', 'info');
            fileInfo.textContent = '';
            aiResponse.innerHTML = 'AI回复将显示在这里...';
            aiAudioPlayer.src = '';

        } catch (error) {
            console.error('启动录音失败:', error);
            showStatus(chatStatus, '错误', '无法启动录音：' + error.message, 'error');
            resetRecordButtons();
        }
    });

    // 停止录音
    stopRecordBtn.addEventListener('click', () => {
        try {
            if (scriptProcessor) {
                scriptProcessor.onaudioprocess = null;
                scriptProcessor.disconnect();
                scriptProcessor = null;
            }

            if (microphone) {
                microphone.disconnect();
                microphone = null;
            }

            if (mediaStream) {
                mediaStream.getTracks().forEach(track => track.stop());
                mediaStream = null;
            }

            // 保存录音文件到后端
            saveAudioToServer();

        } catch (error) {
            console.error('停止录音失败:', error);
            showStatus(chatStatus, '错误', '停止录音时出错', 'error');
        }
    });

    // 开始语音对话 (原HTTP方式)
    startVoiceChatBtn.addEventListener('click', async () => {
        if (!savedFilePath || isProcessing) {
            return;
        }

        try {
            isProcessing = true;
            startRecordBtn.disabled = true;
            startVoiceChatBtn.disabled = true;
            startStreamingVoiceChatBtn.disabled = true;

            // 显示处理状态
            showStatus(chatStatus, '处理中', '正在进行AI对话处理...', 'info');
            aiResponse.innerHTML = 'AI正在思考中...';

            // 调用voice-chat接口
            const response = await axios.post('http://localhost:8080/user/ai/voice-chat', null, {
                params: {filePath: savedFilePath},
                timeout: 60000
            });

            if (response.data.code === 1 && response.data.data) {
                const data = response.data.data;
                // 显示AI回复文本
                aiResponse.innerHTML = data.text || '未获取到文本回复';
                // 设置音频播放器 - 使用base64数据
                if (data.audioBase64) {
                    // 将base64转换为Blob并创建URL
                    const blob = base64ToBlob(data.audioBase64, 'audio/mpeg');
                    const audioUrl = URL.createObjectURL(blob);
                    aiAudioPlayer.src = audioUrl;
                    aiAudioPlayer.load();

                    // 自动播放（注意：浏览器可能会阻止自动播放）
                    try {
                        await aiAudioPlayer.play();
                    } catch (e) {
                        console.warn('自动播放失败，请手动点击播放:', e);
                    }
                }

                showStatus(chatStatus, '成功', 'AI语音对话处理完成', 'success');
            } else {
                showStatus(chatStatus, '错误', 'AI语音对话处理失败：' + (response.data.msg || '未知错误'), 'error');
                aiResponse.innerHTML = '处理失败，请重试';
            }
        } catch (error) {
            console.error('AI语音对话请求错误:', error);
            showStatus(chatStatus, '错误', '请求失败：' + (error.message || '网络错误'), 'error');
            aiResponse.innerHTML = '请求失败，请检查网络连接';
        } finally {
            isProcessing = false;
            startRecordBtn.disabled = false;
        }
    });

    // 开始流式语音对话 (WebSocket方式)
    startStreamingVoiceChatBtn.addEventListener('click', async () => {
        if (!savedFilePath || isProcessing) {
            return;
        }

        try {
            isProcessing = true;
            startRecordBtn.disabled = true;
            startVoiceChatBtn.disabled = true;
            startStreamingVoiceChatBtn.disabled = true;

            // 初始化WebSocket连接
            initWebSocket(savedFilePath);
        } catch (error) {
            console.error('WebSocket连接错误:', error);
            showStatus(chatStatus, '错误', 'WebSocket连接失败：' + (error.message || '未知错误'), 'error');
            isProcessing = false;
            startRecordBtn.disabled = false;
        }
    });

    // 初始化WebSocket连接
    function initWebSocket(filePath) {
        try {
            // 检查浏览器支持
            if (!('WebSocket' in window)) {
                showStatus(chatStatus, '错误', '浏览器不支持WebSocket功能', 'error');
                isProcessing = false;
                startRecordBtn.disabled = false;
                return;
            }

            // 重置音频相关变量
            resetAudioPlayer();

            // 连接WebSocket
            const wsUrl = `ws://localhost:8080/ws/chat`;
            ws = new WebSocket(wsUrl);

            // WebSocket事件处理
            ws.onopen = () => {
                console.log('WebSocket连接已建立');
                showStatus(chatStatus, '连接成功', 'WebSocket连接已建立，正在处理语音...', 'info');
                aiResponse.innerHTML = 'AI正在思考中...';

                // 发送语音聊天请求
                const request = {
                    type: 'voice_chat',
                    filePath: filePath,
                    sessionId: generateUuid(),
                    userId: 1
                };
                ws.send(JSON.stringify(request));
            };

            ws.onmessage = (event) => {
                try {
                    const data = JSON.parse(event.data);
                    console.log('收到WebSocket消息:', data);

                    // 处理不同类型的消息
                    switch (data.type) {
                        case 'connected':
                            showStatus(chatStatus, '连接成功', data.message, 'info');
                            break;
                        case 'status':
                            showStatus(chatStatus, '处理中', data.message, 'info');
                            break;
                        case 'asr_result':
                            // 显示语音识别结果
                            showStatus(chatStatus, '识别成功', '语音识别完成，正在获取AI回复...', 'info');
                            break;
                        case 'stream_text':
                            // 追加流式文本到响应区域
                            appendTextToResponse(data.text);
                            break;
                        case 'audio_segment':
                            // 处理音频片段
                            handleAudioSegment(data.audioData, data.sequence);
                            break;
                        case 'complete':
                            // 处理完成
                            showStatus(chatStatus, '成功', 'AI流式对话处理完成', 'success');
                            // 等待音频播放完成后关闭WebSocket
                            setTimeout(() => {
                                if (!isAudioPlaying) {
                                    closeWebSocket();
                                }
                            }, 1000);
                            break;
                        case 'error':
                            showStatus(chatStatus, '错误', data.message, 'error');
                            aiResponse.innerHTML = '处理失败，请重试';
                            closeWebSocket();
                            break;
                    }
                } catch (e) {
                    console.error('解析WebSocket消息失败:', e);
                }
            };

            ws.onclose = () => {
                console.log('WebSocket连接已关闭');
                // 重置处理状态
                isProcessing = false;
                startRecordBtn.disabled = false;
                // 如果文件已保存，可以重新发起对话
                if (savedFilePath) {
                    startStreamingVoiceChatBtn.disabled = false;
                    startVoiceChatBtn.disabled = false;
                }
            };

            ws.onerror = (error) => {
                console.error('WebSocket连接错误:', error);
                showStatus(chatStatus, '连接错误', 'WebSocket连接异常', 'error');
                closeWebSocket();
                isProcessing = false;
                startRecordBtn.disabled = false;
                if (savedFilePath) {
                    startStreamingVoiceChatBtn.disabled = false;
                    startVoiceChatBtn.disabled = false;
                }
            };

        } catch (error) {
            console.error('初始化WebSocket失败:', error);
            showStatus(chatStatus, '错误', '初始化WebSocket失败：' + error.message, 'error');
            isProcessing = false;
            startRecordBtn.disabled = false;
        }
    }

    // 追加文本到响应区域
    function appendTextToResponse(text) {
        // 检查当前是否是"AI正在思考中..."的默认文本
        if (aiResponse.innerHTML === 'AI正在思考中...') {
            aiResponse.innerHTML = '';
        }

        // 解析Markdown格式，这里简单处理加粗格式
        let processedText = text; // 修复：正确声明变量

        // 处理加粗格式：**文本** -> <strong>文本</strong>
        processedText = processedText.replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>');

        // 追加处理后的文本
        aiResponse.innerHTML += processedText;

        // 滚动到底部
        aiResponse.scrollTop = aiResponse.scrollHeight;
    }

    // 生成音频响应（可选，在对话完成后调用）
    function generateAudioResponse() {
        if (aiResponse.innerHTML && aiResponse.innerHTML !== 'AI回复将显示在这里...' && aiResponse.innerHTML !== 'AI正在思考中...') {
            try {
                showStatus(chatStatus, '处理中', '正在生成语音回复...', 'info');

                // 调用TTS接口生成语音
                axios.post('http://localhost:8080/user/ai/tts', {
                    text: aiResponse.innerHTML
                }).then(response => {
                    if (response.data.code === 1 && response.data.data) {
                        // 设置音频播放器
                        const blob = base64ToBlob(response.data.data, 'audio/mpeg');
                        const audioUrl = URL.createObjectURL(blob);
                        aiAudioPlayer.src = audioUrl;
                        aiAudioPlayer.load();

                        // 自动播放（注意：浏览器可能会阻止自动播放）
                        try {
                            aiAudioPlayer.play();
                        } catch (e) {
                            console.warn('自动播放失败，请手动点击播放:', e);
                        }

                        showStatus(chatStatus, '成功', 'AI流式对话处理完成', 'success');
                    }
                }).catch(error => {
                    console.error('生成语音失败:', error);
                    showStatus(chatStatus, '成功', 'AI流式对话处理完成（语音生成失败）', 'success');
                });
            } catch (error) {
                console.error('生成音频响应失败:', error);
            }
        }
    }

    // 重置音频播放器
    function resetAudioPlayer() {
        // 清理之前的音频资源
        if (audioUrl) {
            URL.revokeObjectURL(audioUrl);
            audioUrl = null;
        }

        // 重置变量
        mediaSource = null;
        sourceBuffer = null;
        audioQueue = [];
        isAudioPlaying = false;

        // 重置音频播放器
        aiAudioPlayer.src = '';
        aiAudioPlayer.load();
    }

    // 处理音频片段
    function handleAudioSegment(audioBase64, sequence) {
        try {
            // 如果是第一个音频片段，初始化MediaSource
            if (!mediaSource) {
                initMediaSource();
            }

            // 将Base64转换为ArrayBuffer
            const byteCharacters = atob(audioBase64);
            const byteNumbers = new Array(byteCharacters.length);
            for (let i = 0; i < byteCharacters.length; i++) {
                byteNumbers[i] = byteCharacters.charCodeAt(i);
            }
            const byteArray = new Uint8Array(byteNumbers);

            // 如果sourceBuffer正在更新，则将音频片段加入队列
            if (sourceBuffer && sourceBuffer.updating) {
                audioQueue.push(byteArray);
            } else {
                // 否则直接添加到sourceBuffer
                appendToSourceBuffer(byteArray);
            }

            // 如果音频尚未开始播放，开始播放
            if (mediaSource && !isAudioPlaying && mediaSource.readyState === 'open') {
                try {
                    aiAudioPlayer.play();
                    isAudioPlaying = true;
                } catch (e) {
                    console.warn('自动播放失败，请手动点击播放:', e);
                    // 可以在这里显示一个提示，让用户点击播放按钮
                }
            }

            // 检查是否为最后一个音频片段
            if (sequence < 0 && audioQueue.length === 0 && sourceBuffer && !sourceBuffer.updating) {
                // 标记流结束
                mediaSource.endOfStream();
                isAudioPlaying = false;
            }
        } catch (error) {
            console.error('处理音频片段失败:', error);
        }
    }

    // 初始化MediaSource
    function initMediaSource() {
        try {
            mediaSource = new MediaSource();

            // 当MediaSource准备就绪时
            mediaSource.addEventListener('sourceopen', () => {
                try {
                    // 为mp3格式创建sourceBuffer
                    const mimeType = 'audio/mpeg';
                    if (MediaSource.isTypeSupported(mimeType)) {
                        sourceBuffer = mediaSource.addSourceBuffer(mimeType);

                        // 监听updateend事件，处理队列中的音频片段
                        sourceBuffer.addEventListener('updateend', () => {
                            if (audioQueue.length > 0 && !sourceBuffer.updating) {
                                const nextAudioChunk = audioQueue.shift();
                                appendToSourceBuffer(nextAudioChunk);
                            }
                        });

                        // 如果队列中已有音频片段，立即处理
                        if (audioQueue.length > 0) {
                            const firstAudioChunk = audioQueue.shift();
                            appendToSourceBuffer(firstAudioChunk);
                        }
                    } else {
                        console.error('浏览器不支持音频格式: ' + mimeType);
                        // 降级处理：使用传统方式播放完整音频
                        fallbackToCompleteAudio();
                    }
                } catch (error) {
                    console.error('初始化sourceBuffer失败:', error);
                    fallbackToCompleteAudio();
                }
            });

            // 设置音频源
            audioUrl = URL.createObjectURL(mediaSource);
            aiAudioPlayer.src = audioUrl;
        } catch (error) {
            console.error('初始化MediaSource失败:', error);
            fallbackToCompleteAudio();
        }
    }

    // 添加音频数据到sourceBuffer
    function appendToSourceBuffer(audioData) {
        try {
            if (sourceBuffer && mediaSource && mediaSource.readyState === 'open') {
                sourceBuffer.appendBuffer(audioData);
            }
        } catch (error) {
            console.error('追加音频数据失败:', error);
        }
    }

    // 降级处理：使用传统方式播放完整音频
    function fallbackToCompleteAudio() {
        console.log('使用降级方案播放音频');
        // 在对话完成后，仍然调用原有的generateAudioResponse方法
        // 这个逻辑已经在complete消息处理中实现
    }

    // 关闭WebSocket连接
    function closeWebSocket() {
        if (ws && ws.readyState === WebSocket.OPEN) {
            ws.close();
        }
    }

    // 生成UUID
    function generateUuid() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }

    // 保存音频文件到服务器
    async function saveAudioToServer() {
        try {
            // 生成唯一文件名
            const timestamp = Date.now();
            const fileName = `voice_chat_${timestamp}.pcm`;

            // 将Int16Array转换为Blob
            const blob = new Blob([new Int16Array(pcmData)], {type: 'application/octet-stream'});

            const formData = new FormData();
            formData.append('audioFile', blob, fileName);

            // 调用保存文件接口
            const response = await axios.post('http://localhost:8080/user/ai/save-audio', formData, {
                timeout: 10000,
                headers: {
                    'Content-Type': 'multipart/form-data'
                }
            });

            if (response.data.code === 1 && response.data.data) {
                savedFilePath = response.data.data;
                console.log('文件保存成功，路径:', savedFilePath);
                showStatus(chatStatus, '录音完成', '录音已保存，请点击对话按钮进行AI处理', 'success');
                fileInfo.textContent = `保存的文件: ${fileName} (${(blob.size / 1024).toFixed(2)} KB)`;
                startVoiceChatBtn.disabled = false;
                startStreamingVoiceChatBtn.disabled = false;
            } else {
                console.error('文件保存失败:', response.data);
                showStatus(chatStatus, '错误', '文件保存失败: ' + (response.data.message || '未知错误'), 'error');
            }
        } catch (error) {
            console.error('保存音频文件失败:', error);
            showStatus(chatStatus, '错误', '保存录音文件失败: ' + error.message, 'error');
        } finally {
            resetRecordButtons();
        }
    }

    // 重置录音按钮状态
    function resetRecordButtons() {
        startRecordBtn.disabled = false;
        stopRecordBtn.disabled = true;
    }

    // 工具函数：显示状态信息
    function showStatus(element, title, message, type) {
        let bgClass = '';
        let icon = '';

        switch (type) {
            case 'success':
                bgClass = 'status-success';
                icon = '✓';
                break;
            case 'error':
                bgClass = 'status-error';
                icon = '✗';
                break;
            case 'info':
                bgClass = 'status-info';
                icon = 'i';
                break;
        }

        element.className = 'status ' + bgClass;
        element.innerHTML = `<strong>${icon} ${title}:</strong> ${message}`;
    }

    // 工具函数：将base64转换为Blob对象
    function base64ToBlob(base64, mimeType) {
        const byteCharacters = atob(base64);
        const byteNumbers = new Array(byteCharacters.length);

        for (let i = 0; i < byteCharacters.length; i++) {
            byteNumbers[i] = byteCharacters.charCodeAt(i);
        }

        const byteArray = new Uint8Array(byteNumbers);
        return new Blob([byteArray], {type: mimeType});
    }
</script>
</body>
</html>